package org.test4j.module.inject.proxy;

import org.test4j.mock.Stubs;
import org.test4j.module.inject.Inject;
import org.test4j.module.inject.Injected;

import java.lang.reflect.Field;
import java.util.*;


public class InjectHelper {

    /**
     * 初始化测试对象的Injected
     *
     * @param testedObject 被测试对象
     */
    public static void injectIntoTestedObject(Object testedObject) {
        Set<Field> injects = getAnnotationFields(testedObject.getClass(), Inject.class);
        for (Field field : injects) {
            Object value = getProxyValue(testedObject, field);
            Inject inject = field.getAnnotation(Inject.class);
            List<Object> targets = targetField(testedObject, inject);
            String[] properties = properties(field, inject);
            for (Object target : targets) {
                injectFieldIntoTarget(target, value, properties);
            }
            if (!inject.stub()) {
                continue;
            }
            try {
                field.setAccessible(true);
                field.set(testedObject, Stubs.fake(field.getType()));
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 先构造代理对象，如果无法构造，直接返回当前值
     *
     * @param testedObject
     * @param field
     * @return
     */
    private static Object getProxyValue(Object testedObject, Field field) {
        try {
            return FieldProxy.proxy(testedObject.getClass(), field);
        } catch (Exception e) {
            return getFieldValue(testedObject, field);
        }
    }

    private static void injectFieldIntoTarget(Object target, Object fieldValue, String[] properties) {
        for (String property : properties) {
            try {
                Field field = getField(target.getClass(), property);
                field.setAccessible(true);
                field.set(target, fieldValue);
            } catch (IllegalAccessException e) {
                throw new RuntimeException("injectFieldIntoTarget, field name:" + property, e);
            }
        }
    }

    private static List<Object> targetField(Object testedObject, Inject inject) {
        List<Object> injected = new ArrayList<>();
        if (inject.targets().length == 0) {
            Set<Field> fields = getAnnotationFields(testedObject.getClass(), Injected.class);
            for (Field field : fields) {
                field.setAccessible(true);
                injected.add(getFieldValue(testedObject, field));
            }
        } else {
            for (String target : inject.targets()) {
                injected.add(getFieldValue(testedObject, target));
            }
        }
        return injected;
    }

    private static Object getFieldValue(Object testedObject, Field field) {
        try {
            field.setAccessible(true);
            return field.get(testedObject);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("getFieldObject[" + field.getName() + "] error:" + e.getMessage(), e);
        }
    }

    private static Object getFieldValue(Object testedObject, String name) {
        Field field = getField(testedObject.getClass(), name);
        return getFieldValue(testedObject, field);
    }

    public static Field getField(Class cls, String name) {
        while (cls != Object.class) {
            try {
                Field field = cls.getDeclaredField(name);
                return field;
            } catch (NoSuchFieldException e) {
                cls = cls.getSuperclass();
            }
        }
        throw new RuntimeException("No such field: " + name);
    }

    private static String[] properties(Field field, Inject inject) {
        if (inject.properties().length == 0) {
            return new String[]{field.getName()};
        } else {
            return inject.properties();
        }
    }

    private static Set<Field> getAnnotationFields(Class clazz, Class annotation) {
        if (Object.class.equals(clazz)) {
            return Collections.emptySet();
        }
        Set<Field> annotatedFields = new HashSet<Field>();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            Object o = field.getAnnotation(annotation);
            if (o != null) {
                annotatedFields.add(field);
            }
        }
        annotatedFields.addAll(getAnnotationFields(clazz.getSuperclass(), annotation));
        return annotatedFields;
    }
}